Skip to content

ES6 Module 模块化面试题全解析

一、核心要点速览

💡 核心考点

  • export/import: ES6 模块化语法
  • default export: 默认导出
  • named export: 具名导出
  • vs CommonJS: 值的引用 vs 值的拷贝
  • tree-shaking: 移除未使用代码

二、export 与 import 基础

1. 导出方式

javascript
// ========== 具名导出(Named Export)==========

// 方式 1:声明时导出
export const PI = 3.14159
export function add(a, b) {
  return a + b
}
export class Circle {
  constructor(radius) {
    this.radius = radius
  }
}

// 方式 2:列表导出
const PI = 3.14159
function add(a, b) {
  return a + b
}
class Circle {}

export { PI, add, Circle }

// 方式 3:重命名导出
export { PI as MathPI, add as sum }


// ========== 默认导出(Default Export)==========

// 一个模块只能有一个 default
export default function multiply(a, b) {
  return a * b
}

// 或者
const config = { apiUrl: '/api' }
export default config

2. 导入方式

javascript
// ========== 具名导入 ==========

// 基本导入
import { PI, add } from './math.js'

// 重命名导入
import { PI as MathPI, add as sum } from './math.js'

// 导入全部
import * as MathUtils from './math.js'
console.log(MathUtils.PI)
console.log(MathUtils.add(1, 2))


// ========== 默认导入 ==========

// 可以自定义名称
import multiply from './math.js'
import config from './config.js'

// 默认 + 具名混合
import multiply, { PI, add } from './math.js'


// ========== 侧边效应导入 ==========

// 只执行模块,不导入内容
import 'polyfill.js'
import './styles.css'

3. 重新导出

javascript
// 重新导出
export { PI, add } from './math.js'

// 重命名后导出
export { PI as MathPI } from './math.js'

// 默认导出转具名
export { default as MathUtils } from './math.js'

// 具名转默认
export { add as default } from './math.js'

// 全部重新导出
export * from './math.js'
export * as MathUtils from './math.js'

三、CommonJS vs ES Module

1. 语法对比

javascript
// ========== CommonJS (Node.js) ==========

// math.js
const PI = 3.14159

function add(a, b) {
  return a + b
}

module.exports = {
  PI,
  add
}

// app.js
const math = require('./math.js')
console.log(math.PI)
console.log(math.add(1, 2))


// ========== ES Module (浏览器/现代 Node) ==========

// math.js
export const PI = 3.14159

export function add(a, b) {
  return a + b
}

// app.js
import { PI, add } from './math.js'
console.log(PI)
console.log(add(1, 2))

2. 核心差异详解

┌──────────────────────────────────────────────────────────┐
│          CommonJS vs ES Module 详细对比                   │
└──────────────────────────────────────────────────────────┘

加载机制:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CommonJS:
  ┌────────────────────────────────┐
  │ 运行时加载                      │
  │ module = require('./mod')      │
  │                                │
  │ 加载整个模块到内存              │
  │ 输出值的拷贝(浅拷贝)         │
  └────────────────────────────────┘

ES Module:
  ┌────────────────────────────────┐
  │ 编译时加载 (静态分析)           │
  │ import { count } from './mod'  │
  │                                │
  │ 输出值的引用(实时绑定)        │
  └────────────────────────────────┘
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

值传递对比:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CommonJS - 值的拷贝:
// mod.js
let count = 1
setTimeout(() => count = 2, 1000)
module.exports = { count }

// main.js
const { count } = require('./mod.js')
console.log(count) // 1
// 1 秒后 count 仍然是 1

ES Module - 值的引用:
// mod.js
export let count = 1
setTimeout(() => count = 2, 1000)

// main.js
import { count } from './mod.js'
console.log(count) // 1
// 1 秒后 count 自动变为 2 ✓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

3. 完整对比表

特性CommonJSES Module
遵循规范CommonJSES6
加载时机运行时编译时
加载方式动态 require()静态 import
this 指向当前模块对象undefined
输出形式值的拷贝值的引用
循环依赖可能不完整正确处理
tree-shaking❌ 不支持✓ 支持
顶层 await✓ 支持✓ 支持
文件扩展名.cjs / .js.mjs / .js
主要环境Node.js浏览器/现代 Node

四、tree-shaking 原理

1. 什么是 tree-shaking

┌──────────────────────────────────────────────────────────┐
│                  tree-shaking 原理                        │
└──────────────────────────────────────────────────────────┘

概念:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
tree-shaking 是一种构建优化技术
用于移除 JavaScript 中未使用的代码

前提条件:
✓ 必须使用 ES Module(静态分析)
✓ 代码必须是纯的(无副作用)
✓ 需要配合构建工具(Webpack、Rollup 等)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

工作流程:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
源代码:
// utils.js
export function used() { 
  return 'used' 
}

export function unused() { 
  return 'unused' 
}

// main.js
import { used, unused } from './utils.js'
console.log(used())

↓ 构建工具分析

发现 unused 从未被使用

↓ 打包时移除 dead code

最终打包结果:
// bundle.js
function used() { return 'used' }
console.log(used())

// unused 函数被摇掉!✓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

2. tree-shaking 示例

javascript
// utils.js
export const usedFunction = () => 'I am used'
export const unusedFunction = () => 'I am unused'

export const USED_CONSTANT = 'used'
export const UNUSED_CONSTANT = 'unused'


// main.js
import { usedFunction, USED_CONSTANT } from './utils.js'

console.log(usedFunction())
console.log(USED_CONSTANT)


// 打包后(优化后):
const usedFunction = () => 'I am used'
const USED_CONSTANT = 'used'

console.log(usedFunction())
console.log(USED_CONSTANT)

// unusedFunction 和 UNUSED_CONSTANT 被移除 ✓


// 优势可视化:
优化前: ████████████████████ 10KB
优化后: ████████████          6KB

体积减少:40% 📦

3. 如何支持 tree-shaking

javascript
// ✓ 好的实践:纯函数
export function add(a, b) {
  return a + b
}

export const PI = 3.14159


// ✗ 不好的实践:有副作用
export const config = (() => {
  console.log('初始化配置') // 副作用
  return { apiUrl: '/api' }
})()

// ✗ 修改全局状态
export function init() {
  window.myApp = {} // 修改全局
}


// ✓ 更好的做法
export function createConfig() {
  return { apiUrl: '/api' }
}

// 在入口文件中调用
import { createConfig } from './config.js'
const config = createConfig()

五、循环依赖问题

1. CommonJS 的循环依赖

javascript
// a.js
const b = require('./b.js')
console.log('a 中的 b:', b)
module.exports = {
  name: 'A',
  getB: () => b
}

// b.js
const a = require('./a.js')
console.log('b 中的 a:', a)
module.exports = {
  name: 'B',
  getA: () => a
}

// 运行结果:
// b 中的 a: {} (空对象,因为 a 还没加载完)
// a 中的 b: { name: 'B', getA: [Function] }

// 问题:可能获取到不完整的导出

2. ES Module 的循环依赖

javascript
// a.js
import b from './b.js'

export const name = 'A'
export default { name, getB: () => b }


// b.js
import a from './a.js'

export const name = 'B'
export default { name, getA: () => a }


// 运行结果:
// ES Module 能正确处理循环依赖
// 因为是在编译时建立依赖关系
// 运行时才执行代码

六、实际应用

1. 模块化项目结构

src/
├── index.js              # 入口文件
├── utils/
│   ├── index.js          # 工具函数汇总
│   ├── string.js         # 字符串工具
│   ├── array.js          # 数组工具
│   └── object.js         # 对象工具
├── components/
│   ├── Button/
│   │   ├── index.js      # 导出组件
│   │   └── Button.vue
│   └── Input/
│       ├── index.js
│       └── Input.vue
└── api/
    ├── index.js          # API 汇总
    ├── user.js           # 用户相关
    └── product.js        # 产品相关


// utils/index.js - 统一导出
export * from './string.js'
export * from './array.js'
export * from './object.js'

// 或按需导出
export { capitalize, trim } from './string.js'
export { flatten, unique } from './array.js'

2. 按需加载

javascript
// 路由懒加载
const routes = [
  {
    path: '/home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/about',
    component: () => import('@/views/About.vue')
  }
]

// 动态 import
async function loadModule(moduleName) {
  const module = await import(`./modules/${moduleName}.js`)
  return module.default
}

// 条件加载
if (condition) {
  const { heavyFunction } = await import('./heavy-module.js')
  heavyFunction()
}

3. 第三方库导入

javascript
// 导入整个库
import _ from 'lodash'
_.map([1, 2, 3], x => x * 2)

// ✓ 更好的做法:按需导入
import map from 'lodash/map'
import filter from 'lodash/filter'

// 或使用 babel-plugin-lodash 自动按需
import { map, filter } from 'lodash'
// 构建时自动转换为单独导入

// Vue 组件导入
import { defineComponent, ref, computed } from 'vue'

// React Hooks 导入
import { useState, useEffect, useCallback } from 'react'

七、面试标准回答

ES6 Module 是 JavaScript 的官方模块标准,使用 export 和 import 关键字来导出和导入模块。

导出方式有两种

  1. 默认导出(default export):一个模块只能有一个,导入时可以自定义名称
  2. 具名导出(named export):可以有多个,导入时必须使用相同的名称或重命名

与 CommonJS 的主要区别

  1. 加载时机:CommonJS 是运行时加载,ES Module 是编译时加载
  2. 输出形式:CommonJS 输出值的拷贝,ES Module 输出值的引用(实时绑定)
  3. 循环依赖:CommonJS 可能获取到不完整的导出,ES Module 能正确处理
  4. tree-shaking:CommonJS 不支持,ES Module 支持移除未使用代码

tree-shaking 的原理是:

  • 利用 ES Module 的静态分析特性
  • 在构建时识别未使用的导出
  • 从最终包中移除这些代码
  • 可以显著减小打包体积(约 30-40%)

实际项目中,我会:

  • 优先使用具名导出(便于 tree-shaking)
  • 默认导出用于导出单个组件或类
  • 使用统一入口文件管理导出
  • 对大型库使用按需导入或动态导入

最佳实践是保持模块的纯粹性(无副作用),这样构建工具才能更好地优化。


八、记忆口诀

Module 模块化歌诀:

ES6 Module 是标准,
export import 来协作。
default 只能有一个,
named 可以有很多!

CommonJS 是老将,
require 来加载。
值拷贝不共享,
tree-shaking 不支持!

ESM 是未来,
值引用实时绑。
静态分析好处多,
构建优化靠它了!

九、推荐资源


十、总结一句话

  • ES Module: 静态导入 + 值的引用 = 现代化模块化 📦
  • tree-shaking: 移除死码 + 减小体积 = 构建优化利器
  • vs CommonJS: 编译时 + 实时绑定 = 更优的选择
最近更新